﻿using System;
using System.Net;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Collections.Generic;
using AccessControl.Contracts.Video;
using System.IO;
using AccessControl.Video.Rtp;
using System.Threading;

namespace AccessControl.Video.Rtsp
{
	/// <summary>
	/// Represents RTSP client
	/// </summary>
	/// <remarks></remarks>
	class RtspClient : SocketBase
	{
		#region Fields

		private uint _sequence;
		private Status _currentStatus;
		private string _authorizationString;

		private byte[] _sps;
		private byte[] _pps; 
		private byte[] _sendBuffer = new byte[4096];
		private byte[] _recvBuffer = new byte[4096];

		private string _track, _session;

		private int _serverRtpPort, _serverRtcpPort;

		private Socket _rtspSocket;
		private RtpSocket _rtpSocket;
		private RtcpSocket _rtcpSocket;

		private List<RtpPacket> _packets = new List<RtpPacket>();

		#endregion

		#region Enums

		private enum Status
		{
			Ready = 0,
			Initializing,
			Describe,
			Setup,
			Play,
			Playing,
			Terminating
		}

		#endregion

		#region Public Constructor
		
		public RtspClient()
		{
			_authorizationString = Convert.ToBase64String(ASCIIEncoding.Default.GetBytes("admin:1234"));
		}
		
		#endregion

		#region Public Members

		/// <summary>
		/// Initializes this instance.
		/// </summary>
		/// <returns></returns>
		/// <remarks></remarks>
		public override bool Initialize()
		{
			lock (this)
			{
				/* Validate the status */
				if (_currentStatus != Status.Ready)
					return false;

				/* First try to connect */
				_rtspSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
				_rtspSocket.BeginConnect(new IPEndPoint(IP, PortNumber), OnReplyConnect, null);
				return true;
			}
		}

		/// <summary>
		/// Terminates this instance.
		/// </summary>
		/// <returns></returns>
		/// <remarks></remarks>
		public override bool Terminate()
		{
            lock (this)
            {
                /* Validate the status */
                if (_currentStatus != Status.Playing)
                    return false;

                /* Set terminating status */
                _currentStatus = Status.Terminating;
                /* And send the teardown command */
                SendTeradownMethod();
            }

			return true;
		}

        #endregion

		#region Public Events

		public event NewFrameEventHandler NewFrame;
		public event VideoEventHandler Initialized;
		public event VideoEventHandler Terminated;

		#endregion

		#region Private Members

		private void OnReplyConnect(IAsyncResult result)
		{
			lock (this)
			{
				try
				{
					/* Finish the connect operation */
					_rtspSocket.EndConnect(result);
				}
				catch
				{
					CloseClient();
					return;
				}

				/* Now we need to send the describe method */
				SendDescribeMethod();
			}

		}

		private void SendDescribeMethod()
		{
			string sDescribe = string.Empty;

			lock (this)
			{
				System.Diagnostics.Debug.WriteLine("Building describe");
				/* Build the message */
				sDescribe += string.Format("DESCRIBE rtsp://{0}/ RTSP/1.0\r\n", IP.ToString());
				sDescribe += string.Format("CSeq: {0}\r\n", ++_sequence);
				sDescribe += string.Format("Accept: {0}\r\n", "application/sdp");
				
				if (!string.IsNullOrEmpty(_authorizationString))
					sDescribe += string.Format("Authorization: Basic {0}\r\n", _authorizationString);

				sDescribe += "\r\n";

				/* Convert to bytes */
				_sendBuffer = ASCIIEncoding.Default.GetBytes(sDescribe);
				/* Send */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Sending describe");
					_rtspSocket.BeginSend(_sendBuffer, 0, _sendBuffer.Length, SocketFlags.None, OnReplySendDescribeMethod, null); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed to send describe");
					CloseClient();
					return;
				}
				/* And set the matching state */
				_currentStatus = Status.Describe;
			}
		}

		private void OnReplySendDescribeMethod(IAsyncResult result)
		{
			lock (this)
			{
				try 
				{
					System.Diagnostics.Debug.WriteLine("End sending describe");
					_rtspSocket.EndSend(result); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed to end send describe");
					CloseClient();
					return;
				}

				/* Now read the describe response */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Begining recv describe");
					_rtspSocket.BeginReceive(_recvBuffer, 0, _recvBuffer.Length, SocketFlags.None, OnReadDescribeReply, null); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed Begining recv describe");
					CloseClient();
					return;
				}
			}
		}

		private void OnReadDescribeReply(IAsyncResult result)
		{
			int nRead = 0;
			string sReply;

			lock(this)
			{
				try
				{
					System.Diagnostics.Debug.WriteLine("End recv describe");
					nRead = _rtspSocket.EndReceive(result);
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed end recv describe");
					CloseClient();
					return;
				}

				if (nRead <= 0)
				{
					CloseClient();
					return;
				}

				/* Parse the reply */
				sReply = ASCIIEncoding.Default.GetString(_recvBuffer, 0, nRead);
				System.Diagnostics.Debug.Write("Describe reply is: " + sReply);

				/* Find the SPS and PPS */
				StringReader sReader = new StringReader(sReply);
				string sLine;
				while ((sLine = sReader.ReadLine()) != null)
				{
					if (sLine.Contains("sprop-parameter-sets="))
					{
						sLine = sLine.Remove(0, sLine.IndexOf("sprop-parameter-sets="));
						sLine = sLine.Replace("sprop-parameter-sets=", "");
						sLine = sLine.Replace("\r\n", "");
						string[] spspps = sLine.Split(',');
						_sps = Convert.FromBase64String(spspps[0]);
						_pps = Convert.FromBase64String(spspps[1]);
						break;
					}
				}

				/* Find ports for the rtp and rtcp */
				if (!AllocateSockets())
				{
					CloseClient();
					return;
				}

				/* Now send the setup command */
				_track = "trackID=1";
				//_track = "trackID=2";
				SendSetupMethod();
			}
		}

		private bool AllocateSockets()
		{
			UdpClient udp;
			bool bFound = false;
			IPEndPoint ipEndPoint;
			int rtpPort, rtcpPort;

			while (!bFound)
			{
				/* First we need to allocate the rtp */
				do
				{
					udp = new UdpClient(0);
					ipEndPoint = udp.Client.LocalEndPoint as IPEndPoint;
					rtpPort = ipEndPoint.Port;
					udp.Close();
				}
				while ((rtpPort % 2) != 0);
				
				/* Ok. now we have the rtp port. we need +1 for the rtcp port */
				try 
				{ 
					udp = new UdpClient(rtpPort + 1); 
					udp.Close();
				}
				catch
				{ 
					/* Again */
					rtpPort = -1;
					continue; 
				}


				/* We are ok. Init */
				_rtpSocket = new RtpSocket() { IP = IP, PortNumber = rtpPort, SPS = _sps, PPS = _pps };
				_rtpSocket.NewRtpPacket += (OnNewRtpPacket);
				_rtcpSocket = new RtcpSocket() { IP = IP, PortNumber = rtpPort + 1 };
				bFound = true;
			}

			return _rtpSocket.Initialize() && _rtcpSocket.Initialize();
		}

		void OnNewRtpPacket(RtpPacket packet)
		{
			RtpPacket[] aPackets;
			/* Hand over to the rtcp handler */
			_rtcpSocket.HandleNewRtpPacket(packet);
			/* Add to our list */
			_packets.Add(packet);
			/* And if it is the last one */
			if (packet.IsLastPacketInFrame)
			{
				aPackets = _packets.ToArray();
				new Thread(new ParameterizedThreadStart(BuildAndSendFrame)).Start(aPackets);
				_packets.Clear();
			}
		}

		private void BuildAndSendFrame(object param)
		{
			byte[] frame;
			int count = 0, idx = 0;
			RtpPacket[] packets = (RtpPacket[])param;


			foreach (RtpPacket packet in packets)
				count += packet.Payload.Length;

			/* Allocate the buffer */
			frame = new byte[count];
			/* And copy */
			foreach (RtpPacket packet in packets)
			{
				Array.Copy(packet.Payload, 0, frame, idx, packet.Payload.Length);
				idx += packet.Payload.Length;
			}

			/* And raise the frame event */
			if (NewFrame != null)
			{
				//NewFrame(frame, DateTime.Now, 640, 480);
				NewFrame(frame, DateTime.Now, 960, 592);
			}
		}

		private void SendSetupMethod()
		{
			string sCommand = string.Empty;

			lock (this)
			{
				System.Diagnostics.Debug.WriteLine("Building setup");
				/* Build the message */
				//sCommand += string.Format("SETUP rtsp://{0}/live2.sdp/{1} RTSP/1.0\r\n", IP.ToString(), _track);
				sCommand += string.Format("SETUP rtsp://{0}/{1} RTSP/1.0\r\n", IP.ToString(), _track);
				sCommand += string.Format("CSeq: {0}\r\n", ++_sequence);
				sCommand += string.Format("Transport: RTP/AVP;unicast;client_port={0}-{1}\r\n", _rtpSocket.PortNumber, _rtcpSocket.PortNumber);
				sCommand += "\r\n";

				/* Convert to bytes */
				_sendBuffer = ASCIIEncoding.Default.GetBytes(sCommand);
				/* Send */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Start sending setup");
					_rtspSocket.BeginSend(_sendBuffer, 0, _sendBuffer.Length, SocketFlags.None, OnReplySendSetupMethod, null); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed Start sending setup");
					CloseClient();
					return;
				}
				/* And set the matching state */
				_currentStatus = Status.Setup;
			}
		}

		private void OnReplySendSetupMethod(IAsyncResult result)
		{
			lock (this)
			{
				try 
				{
					System.Diagnostics.Debug.WriteLine("End sending setup");
					_rtspSocket.EndSend(result); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed End sending setup");
					CloseClient();
					return;
				}

				/* Now read the setup response */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Start recv setup");
					_rtspSocket.BeginReceive(_recvBuffer, 0, _recvBuffer.Length, SocketFlags.None, OnReadSetupReply, null); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed Start recv setup");
					CloseClient();
					return;
				}
			}
		}

		private void OnReadSetupReply(IAsyncResult result)
		{
			int nRead = 0;
			string sReply;

			lock (this)
			{
				try
				{
					System.Diagnostics.Debug.WriteLine("End recv setup");
					nRead = _rtspSocket.EndReceive(result);
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed End recv setup");
					CloseClient();
					return;
				}

				if (nRead <= 0)
				{
					CloseClient();
					return;
				}

				System.Diagnostics.Debug.WriteLine("Parse setup");
				/* Parse the reply */
				sReply = ASCIIEncoding.Default.GetString(_recvBuffer, 0, nRead);
				System.Diagnostics.Debug.Write("Setup reply is: " + sReply);
				/* Gte the session */
				StringReader sReader = new StringReader(sReply);
				string sLine = sReader.ReadLine();
				while (sLine != null)
				{
					if (sLine.Contains("Session: "))
					{
						_session = sLine.Replace("Session: ", "");
						_session = _session.Replace("\r\n", "");						
					}
					else if (sLine.Contains("server_port"))
					{
						int idx = sLine.IndexOf("server_port=");
						string serverPorts = sLine.Remove(0, idx);
						serverPorts = serverPorts.Replace("server_port=", "");
						string[] ports = serverPorts.Split('-');
						_serverRtpPort = Convert.ToInt32(ports[0]);
						_serverRtcpPort = Convert.ToInt32(ports[1]);
					}

					sLine = sReader.ReadLine();
				}

				/* Set the ports and ip address to the sockets */
				_rtcpSocket.ServerAddress = this.IP;
				_rtcpSocket.ServerRtcpPort = _serverRtcpPort;

				/* Now send the play command */
				SendPlayMethod();
			}
		}

		private void SendPlayMethod()
		{
			string sCommand = string.Empty;

			lock (this)
			{
				System.Diagnostics.Debug.WriteLine("Build play");
				/* Build the message */
				sCommand += string.Format("PLAY rtsp://{0}/ RTSP/1.0\r\n", IP.ToString());
				sCommand += string.Format("CSeq: {0}\r\n", ++_sequence);
				sCommand += string.Format("Session: {0}\r\n", _session);
				sCommand += "\r\n";

				/* Convert to bytes */
				_sendBuffer = ASCIIEncoding.Default.GetBytes(sCommand);
				/* Send */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Start sending play");
					_rtspSocket.BeginSend(_sendBuffer, 0, _sendBuffer.Length, SocketFlags.None, OnReplySendPlayMethod, null); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed Start sending play");
					CloseClient();
					return;
				}
				/* And set the matching state */
				_currentStatus = Status.Play;
			}
		}

		private void OnReplySendPlayMethod(IAsyncResult result)
		{
			lock (this)
			{
				try 
				{
					System.Diagnostics.Debug.WriteLine("End sending play");
					_rtspSocket.EndSend(result); 
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed End sending play");
					CloseClient();
					return;
				}

				/* Now read the setup response */
				try 
				{
					System.Diagnostics.Debug.WriteLine("Staer recv play");
					_rtspSocket.BeginReceive(_recvBuffer, 0, _recvBuffer.Length, SocketFlags.None, OnReadPlayReply, null); 
				}
				catch
				{
					CloseClient();
					return;
				}
			}
		}

		private void OnReadPlayReply(IAsyncResult result)
		{
			int nRead = 0;
			string sReply;

			lock (this)
			{
				try
				{
					System.Diagnostics.Debug.WriteLine("End recv play");
					nRead = _rtspSocket.EndReceive(result);
				}
				catch
				{
					System.Diagnostics.Debug.WriteLine("Failed End recv play");
					CloseClient();
					return;
				}

				if (nRead <= 0)
				{
					CloseClient();
					return;
				}

				System.Diagnostics.Debug.WriteLine("Parse play");
				/* Parse the reply */
				sReply = ASCIIEncoding.Default.GetString(_recvBuffer, 0, nRead);
				System.Diagnostics.Debug.Write("Play reply is: " + sReply);
				/* And we are done */
				_currentStatus = Status.Playing;
				if (Initialized != null)
					Initialized(true);
			}
		}

        private void SendTeradownMethod()
        {
            string sCommand = string.Empty;

            lock (this)
            {
                System.Diagnostics.Debug.WriteLine("Build teardown");
                /* Build the message */
                sCommand += string.Format("TEARDOWN rtsp://{0}/ RTSP/1.0\r\n", IP.ToString());
                sCommand += string.Format("CSeq: {0}\r\n", ++_sequence);
                sCommand += string.Format("Session: {0}\r\n", _session);
                sCommand += "\r\n";

                /* Convert to bytes */
                _sendBuffer = ASCIIEncoding.Default.GetBytes(sCommand);
                /* Send */
                try
                {
                    System.Diagnostics.Debug.WriteLine("Start sending teardown");
                    _rtspSocket.BeginSend(_sendBuffer, 0, _sendBuffer.Length, SocketFlags.None, OnReplySendTeardownMethod, null);
                }
                catch
                {
                    System.Diagnostics.Debug.WriteLine("Failed Start sending teardown");
                    CloseClient();
                    return;
                }                
            }
        }

        private void OnReplySendTeardownMethod(IAsyncResult result)
        {
            lock (this)
            {
                try
                {
                    System.Diagnostics.Debug.WriteLine("End sending teardown");
                    _rtspSocket.EndSend(result);
                }
                catch
                {
                    System.Diagnostics.Debug.WriteLine("Failed End sending teardown");
                    CloseClient();
                    return;
                }

                /* Now read the teardown response */
                try
                {
                    System.Diagnostics.Debug.WriteLine("Start recv teardown");
                    _rtspSocket.BeginReceive(_recvBuffer, 0, _recvBuffer.Length, SocketFlags.None, OnReadTeardownReply, null);
                }
                catch
                {
                    CloseClient();
                    return;
                }
            }
        }

        private void OnReadTeardownReply(IAsyncResult result)
        {
            int nRead = 0;
            string sReply;

            lock (this)
            {
                try
                {
                    System.Diagnostics.Debug.WriteLine("End recv teardown");
                    nRead = _rtspSocket.EndReceive(result);
                }
                catch
                {
                    System.Diagnostics.Debug.WriteLine("Failed End recv teardown");
                    CloseClient();
                    return;
                }

                if (nRead <= 0)
                {
                    CloseClient();
                    return;
                }

                System.Diagnostics.Debug.WriteLine("Parse teardown");
                /* Parse the reply */
                sReply = ASCIIEncoding.Default.GetString(_recvBuffer, 0, nRead);
                System.Diagnostics.Debug.Write("Teardown reply is: " + sReply);
                /* And we are done */
                CloseClient();
            }
        }



		private void CloseClient()
		{
			try { _rtpSocket.Terminate(); }
			catch { }
			finally { _rtpSocket = null; }

			try { _rtcpSocket.Terminate(); }
			catch { }
			finally { _rtcpSocket = null; }

			try { _rtspSocket.Close(); }
			catch { }
			finally { _rtspSocket = null; }

            if (_currentStatus == Status.Terminating)
            {
                if (Terminated != null)
                    Terminated(true);
            }
            else
            {
                if (Initialized != null)
                    Initialized(false);
            }

            /* And we are ready now */
            _currentStatus = Status.Ready;
        }

		#endregion
	}
}
